1use super::reach_set2::{ReachSet2Options, reach_set2_backward};
20use crate::copp::copp2::formulation::Topp2Problem;
21use crate::copp::{ApproxOrdering, approx_order};
22use crate::diag::{
23 CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
24 format_duration_human,
25};
26use crate::math::numerical::{LpToleranceOptions, lp_1d};
27use core::f64;
28use itertools::izip;
29
30pub fn topp2_ra(problem: &Topp2Problem, options: &ReachSet2Options) -> Result<Vec<f64>, CoppError> {
48 match options.verbosity {
49 Verbosity::Silent => topp2_ra_core(problem, (options, SilentVerboser)),
50 Verbosity::Summary => topp2_ra_core(problem, (options, SummaryVerboser::new())),
51 Verbosity::Debug => topp2_ra_core(problem, (options, DebugVerboser::new())),
52 Verbosity::Trace => topp2_ra_core(problem, (options, TraceVerboser::new())),
53 }
54}
55
56fn topp2_ra_core(
58 problem: &Topp2Problem,
59 options_verboser: (&ReachSet2Options, impl Verboser),
60) -> Result<Vec<f64>, CoppError> {
61 let (options, mut verboser) = options_verboser;
62 if verboser.is_enabled(Verbosity::Summary) {
63 verboser.record_start_time();
64 crate::verbosity_log!(
65 Verbosity::Summary,
66 "\ntopp2_ra started: {} <= idx_s <= {}, a_start = {}, a_final = {}.",
67 problem.idx_s_interval.0,
68 problem.idx_s_interval.1,
69 problem.a_boundary.0,
70 problem.a_boundary.1,
71 );
72 }
73
74 let reach_set = reach_set2_backward(problem, options).map_err(|e| {
76 if verboser.is_enabled(Verbosity::Debug) {
77 crate::verbosity_log!(Verbosity::Debug, "{e:?}");
78 } else if verboser.is_enabled(Verbosity::Summary) {
79 crate::verbosity_log!(
80 Verbosity::Summary,
81 "topp2_ra: failed while computing backward reachable set."
82 );
83 }
84 e
85 })?;
86 let a_max = &reach_set.a_max;
87 let a_min = &reach_set.a_min;
88
89 if verboser.is_enabled(Verbosity::Debug) {
91 crate::verbosity_log!(Verbosity::Debug, "Forward pass started.");
92 }
93
94 let (idx_s_start, idx_s_final) = problem.idx_s_interval;
95 let n = idx_s_final - idx_s_start;
96 let mut a = vec![0.0; n + 1];
97 let mut a_prev = problem.a_boundary.0;
98 *a.first_mut().unwrap() = a_prev;
99
100 let mut a_b = Vec::<(f64, f64, f64)>::with_capacity(2 * problem.constraints.acc_rows());
101 for (k, (a_curr, &a_max_curr_, &a_min_curr_)) in
102 izip!(a.iter_mut(), a_max, a_min).enumerate().skip(1)
103 {
104 let idx_s = idx_s_start + k;
105 if verboser.is_enabled(Verbosity::Trace) {
106 crate::verbosity_log!(
107 Verbosity::Trace,
108 "\tForward pass at k = {k} (idx_s = {idx_s}): backward interval {a_min_curr_} <= a[k] <= {a_max_curr_}, a_prev = {a_prev}."
109 );
110 }
111
112 a_b.clear();
113 problem
114 .constraints
115 .fill_acc_topp2::<true>(&mut a_b, idx_s - 1);
116 let (mut a_max_curr, mut a_min_curr) = lp_1d::<true>(
118 a_b.iter().map(|&coeffs| {
119 (coeffs.0, coeffs.2 - coeffs.1 * a_prev)
122 }),
123 &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
124 );
125
126 if verboser.is_enabled(Verbosity::Trace) {
127 crate::verbosity_log!(
128 Verbosity::Trace,
129 "\t\tForward LP result before clipping: {a_min_curr} <= a[k] <= {a_max_curr}."
130 );
131 }
132
133 a_max_curr = a_max_curr.min(a_max_curr_);
134 a_min_curr = a_min_curr.max(a_min_curr_);
135 if verboser.is_enabled(Verbosity::Trace) {
136 crate::verbosity_log!(
137 Verbosity::Trace,
138 "\t\tAfter clipping with backward reachable set: {a_min_curr} <= a[k] <= {a_max_curr}."
139 );
140 }
141
142 if a_max_curr.is_nan()
143 || a_min_curr.is_nan()
144 || matches!(
145 approx_order(
146 a_max_curr,
147 a_min_curr,
148 options.a_cmp_abs_tol,
149 options.a_cmp_rel_tol,
150 ),
151 ApproxOrdering::Less
152 )
153 {
154 let err = CoppError::Infeasible(
155 "topp2_ra".into(),
156 format!(
157 "The reachable set is empty at index {} during the forward pass where a_max = {}, a_min = {}",
158 idx_s_start + k,
159 a_max_curr,
160 a_min_curr
161 ),
162 );
163 if verboser.is_enabled(Verbosity::Debug) {
164 crate::verbosity_log!(Verbosity::Debug, "{err:?}");
165 } else if verboser.is_enabled(Verbosity::Summary) {
166 crate::verbosity_log!(
167 Verbosity::Summary,
168 "topp2_ra: the forward pass failed at index {idx_s} due to infeasibility."
169 );
170 }
171 return Err(err);
172 }
173
174 if a_max_curr.is_infinite() {
175 let err = CoppError::Unbounded(
176 "topp2_ra".into(),
177 format!(
178 "The reachable set is unbounded at index {} during the forward pass where a_max = {}",
179 idx_s_start + k,
180 a_max_curr
181 ),
182 );
183 if verboser.is_enabled(Verbosity::Debug) {
184 crate::verbosity_log!(Verbosity::Debug, "{err:?}");
185 } else if verboser.is_enabled(Verbosity::Summary) {
186 crate::verbosity_log!(
187 Verbosity::Summary,
188 "topp2_ra: the forward pass failed at index {idx_s} due to unboundedness."
189 );
190 }
191 return Err(err);
192 }
193
194 if verboser.is_enabled(Verbosity::Debug)
195 && matches!(
196 approx_order(
197 a_max_curr,
198 a_min_curr,
199 options.a_cmp_abs_tol,
200 options.a_cmp_rel_tol,
201 ),
202 ApproxOrdering::Equal
203 )
204 {
205 crate::verbosity_log!(
206 Verbosity::Debug,
207 "The forward reachable set at k = {k} (idx_s = {idx_s}) is degenerate since a_max and a_min are approximately equal at {}.",
208 0.5 * (a_max_curr + a_min_curr)
209 );
210 }
211
212 a_prev = a_max_curr;
213 *a_curr = a_prev;
214
215 if verboser.is_enabled(Verbosity::Trace) {
216 crate::verbosity_log!(
217 Verbosity::Trace,
218 "\t\tSelected maximal feasible state: a[k] = {a_prev}."
219 );
220 }
221 }
222
223 if verboser.is_enabled(Verbosity::Summary) {
224 crate::verbosity_log!(
225 Verbosity::Summary,
226 "topp2_ra: total elapsed time = {}.\n",
227 format_duration_human(verboser.elapsed())
228 );
229 }
230
231 Ok(a)
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::copp::InterpolationMode;
238 use crate::copp::copp2::stable::basic::{
239 Topp2ProblemBuilder, a_to_b_topp2, s_to_t_topp2, t_to_s_topp2,
240 };
241 use crate::copp::copp2::stable::reach_set2::ReachSet2OptionsBuilder;
242 use crate::path::{add_symmetric_axial_limits_for_test, lissajous_path_for_test};
243 use crate::robot::robot_core::Robot;
244 use std::time::{Duration, Instant};
245
246 #[test]
247 fn test_topp2_ra() -> Result<(), CoppError> {
248 run_test_topp2_ra_repeated(1, false)
249 }
250
251 #[test]
254 #[ignore = "slow"]
255 fn test_topp2_ra_robust() -> Result<(), CoppError> {
256 run_test_topp2_ra_repeated(10000, true)
257 }
258
259 fn run_test_topp2_ra_repeated(n_exp: usize, flag_print_step: bool) -> Result<(), CoppError> {
260 let mut tc_sum_ra = Duration::ZERO;
261 let mut tc_sum_interpolation = Duration::ZERO;
262 let mut t_final_sum = 0.0;
263
264 let options = ReachSet2OptionsBuilder::new()
265 .lp_feas_tol(1E-9)
266 .a_cmp_abs_tol(1E-9)
267 .a_cmp_rel_tol(1E-9)
268 .verbosity(Verbosity::Summary)
269 .build()?;
270
271 for i_exp in 0..n_exp {
272 let dim = 7;
273 let n: usize = 1000;
274 let mut robot = Robot::with_capacity(dim, n);
275
276 let mut rng = rand::rng();
277 let (s, derivs, _, _) = lissajous_path_for_test(dim, n, &mut rng).map_err(|e| {
278 CoppError::InvalidInput("lissajous_path_for_test".into(), e.to_string())
279 })?;
280 robot.with_s(&s.as_view())?;
281 robot.with_q(
282 &derivs.q.as_view(),
283 &derivs.dq.as_ref().unwrap().as_view(),
284 &derivs.ddq.as_ref().unwrap().as_view(),
285 derivs.dddq.as_ref().map(|m| m.as_view()).as_ref(),
286 0,
287 )?;
288 add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, None)?;
289
290 let start = Instant::now();
291 let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
292 let a_profile = topp2_ra(&topp2_problem, &options)?;
293 let tc_topp2_ra = start.elapsed();
294
295 let b_profile = a_to_b_topp2(s.as_slice(), &a_profile);
296 assert!(
297 !izip!(
298 s.as_slice().windows(2),
299 a_profile.windows(2),
300 b_profile.iter()
301 )
302 .any(|(s_pair, a_pair, b)| {
303 let ds_double = 2.0 * (s_pair[1] - s_pair[0]);
304 let db = (a_pair[1] - a_pair[0]) / ds_double;
305 (*b - db).abs() > 1e-3
306 }),
307 "b_profile generation failed!"
308 );
309
310 let start = Instant::now();
311 let (t_final, t_s) = s_to_t_topp2(s.as_slice(), &a_profile, 0.0);
312 assert_eq!(t_s.len(), s.ncols());
313 let tc_interpolation = start.elapsed();
314 let s_t = t_to_s_topp2(
315 s.as_slice(),
316 &a_profile,
317 &t_s,
318 InterpolationMode::UniformTimeGrid(0.0, 1E-3, true),
319 );
320
321 tc_sum_ra += tc_topp2_ra;
322 tc_sum_interpolation += tc_interpolation;
323 t_final_sum += t_final;
324
325 if flag_print_step && ((i_exp + 1) % 100 == 0) {
326 crate::verbosity_log!(
327 Verbosity::Summary,
328 "Exp #{}: tc_topp2_ra = {:.4} ms, tc_interpolation = {:.4} ms, t_final = {:.4} s, s_t.len() = {}",
329 i_exp + 1,
330 tc_topp2_ra.as_secs_f64() * 1E3,
331 tc_interpolation.as_secs_f64() * 1E3,
332 t_final,
333 s_t.len()
334 );
335 }
336 }
337
338 crate::verbosity_log!(
339 Verbosity::Summary,
340 "Average over {} experiments: tc_topp2_ra = {:.6} ms, tc_interpolation = {:.6} ms, t_final = {:.6}",
341 n_exp,
342 tc_sum_ra.as_secs_f64() * 1E3 / n_exp as f64,
343 tc_sum_interpolation.as_secs_f64() * 1E3 / n_exp as f64,
344 t_final_sum / n_exp as f64
345 );
346
347 Ok(())
348 }
349}